package com.dreangine.exception.util;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.dreangine.exception.DebuggerException;

/**
 * This class has the purpose of easing the task of creating and managing
 * the properties file for the {@link DebuggerException}.
 * 
 * @author Omar V. Buede
 * @version 1.3
 * @see DebuggerException
 */
public class DebuggerExceptionUtil {
	private static final String DEFAULTS_TEXT = "Defaults"; //$NON-NLS-1$
	private static final String DEFAULT_MESSAGE = "<auto-generated message>"; //$NON-NLS-1$
	private static final String DEFAULT_MESSAGE_REGEX = "defaultMessage"; //$NON-NLS-1$
	private static final String AUTO_GENERATED_ENTRIES_MESSAGE = "Auto-generated entries"; //$NON-NLS-1$
	private static final String AUTO_GENERATED_NEW_ENTRIES_MESSAGE = "NEW ENTRIES"; //$NON-NLS-1$
	private static final String AUTO_GENERATED_UNUSED_ENTRIES_MESSAGE = "UNUSED ENTRIES"; //$NON-NLS-1$
	private static final String TEMP_PROPS_FILE_PREFIX = "exceptions_properties"; //$NON-NLS-1$
	private static final String TEMP_PROPS_FILE_SUFFIX = ".tmp"; //$NON-NLS-1$
	private static final String DEBUG_REGEX = "DEBUG"; //$NON-NLS-1$
	private static final String FALSE_REGEX = "false"; //$NON-NLS-1$
	private static final String LINE_REGEX = "line"; //$NON-NLS-1$
	private static final String CLASS_FILE_REGEX = "[a-zA-Z0-9_\\-.]+.class"; //$NON-NLS-1$
	private static final String CLASS_FILE_PATH_REGEX = "[\\" + File.separator + "]+"; //$NON-NLS-1$ //$NON-NLS-2$
	private static final String EQUALS = "="; //$NON-NLS-1$
	private static final String DOT = "."; //$NON-NLS-1$
	private static final String HASH = "#"; //$NON-NLS-1$
	private static final String SPACE = " "; //$NON-NLS-1$
	
	/**
	 * No instance is necessary since this class has no state.
	 */
	private DebuggerExceptionUtil() {
		// Private constructor to prevent instances
	}
	
	/**
	 * The main. It will be called upon the JAR execution.
	 * 
	 * @param args parent dir and properties file
	 * @throws FileNotFoundException
	 * @throws IOException
	 * @throws ClassNotFoundException
	 */
	public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
		String errorMessage;
		
		errorMessage = "Both parent dir and properties file path must be informed!"; //$NON-NLS-1$
		File dir, props;
		if(args.length == 2) {
			File arg1, arg2;
			
			arg1 = new File(args[0]);
			arg2 = new File(args[1]);
			
			if(arg1.isDirectory()) {
				if(arg2.isFile()) {
					props = arg2;
				}
				else {
					throw new IllegalArgumentException(errorMessage);
				}
				dir = arg1;
			}
			else if(arg2.isDirectory()) {
				props = arg1;
				dir = arg2;
			}
			else {
				throw new IllegalArgumentException(errorMessage);
			}
		}
		else {
			throw new IllegalArgumentException(errorMessage);
		}
		generateMissingEntries(dir, props);
	}
	
	/**
	 * This method searches for entries that are not on the properties file and
	 * for the ones that are on the file but are not used anymore and then updates
	 * the file with the collected information.
	 * 
	 * @param parentDir the root directory for the packages and the class files
	 * @param propsFile the properties file that will be checked
	 * @throws IOException 
	 * @throws FileNotFoundException 
	 * @throws ClassNotFoundException 
	 */
	public static void generateMissingEntries(File parentDir, File propsFile)
			throws FileNotFoundException, IOException, ClassNotFoundException {
		Map<String, List<String>> cmMap, cmMapDefaults;
		List<String> tempMethodsList, tempDefaultsList;
		List<File> files;
		String tempClassName;
		Class<?> tempClass;

		tempDefaultsList = generateDefaults();
		tempDefaultsList = checkPropsFile(propsFile, tempDefaultsList);
		cmMapDefaults = new HashMap<String, List<String>>();
		if(!tempDefaultsList.isEmpty()) {
			cmMapDefaults.put(DEFAULTS_TEXT, tempDefaultsList);
		}
		if(!cmMapDefaults.isEmpty()) {
			updatePropsFile(propsFile, cmMapDefaults);
		}
		cmMap = new HashMap<String, List<String>>();
		tempMethodsList = new ArrayList<String>();
		files = findClassFiles(parentDir);
		for (File file : files) {
			tempClassName = extractClassName(file);
			try {
				tempClass = getClass(tempClassName);
				tempMethodsList = getMethods(tempClass);
			} catch (ClassNotFoundException cnfe) {
				System.err.println(cnfe);
				continue;
			}
			tempMethodsList = checkPropsFile(propsFile, tempMethodsList);
			if(!tempMethodsList.isEmpty()) {
				cmMap.put(tempClassName, tempMethodsList);
			}
		}
		if(!cmMap.isEmpty()) {
			updatePropsFile(propsFile, cmMap);
		}
	}
	
	/**
	 * This method will generate the default configuration lines that must be present on the
	 * properties file.
	 * 
	 * @return a list containing the default configuration lines
	 */
	private static List<String> generateDefaults() {
		List<String> defaultsList;

		defaultsList = new ArrayList<String>();
		defaultsList.add(DEBUG_REGEX);
		defaultsList.add(LINE_REGEX);
		defaultsList.add(DEFAULT_MESSAGE_REGEX);
		
		return defaultsList;
	}
	
	/**
	 * This method will search for any .class file inside the given directory and
	 * it's subsequent directories.
	 * 
	 * @param parentDir the parent directory to be searched
	 * @return a list of .class files
	 * @throws Exception
	 */
	private static List<File> findClassFiles(File parentDir) {
		List<File> classFiles;
		File tempFile;
		String[] dirContent;
		Pattern pat;
		Matcher mat;
		
		pat = Pattern.compile(CLASS_FILE_REGEX);
		classFiles = new ArrayList<File>();
		dirContent = parentDir.list();
		for (String string : dirContent) {
			tempFile = new File(parentDir + File.separator + string);
			mat = pat.matcher(string);
			if(mat.find()) {
				classFiles.add(tempFile);
			}
			else if(tempFile.isDirectory()) {
				classFiles.addAll(findClassFiles(tempFile));
			}
		}
		
		return classFiles;
	}
	
	/**
	 * This method will check the informed properties file and will search it for
	 * occurrences of the informed methods.
	 * 
	 * @param propsFile the properties file
	 * @param methods a list with the methods that will be searched inside the properties file
	 * @return a list containing only the informed methods that are not present on the properties file
	 * @throws FileNotFoundException 
	 * @throws IOException 
	 */
	public static List<String> checkPropsFile(File propsFile,
			List<String> methods) throws FileNotFoundException, IOException {
		FileInputStream fis;
		List<String> newMethods;
		Set<Object> propsKeys;
		Properties props;
		
		if(propsFile.exists()) {
			fis = new FileInputStream(propsFile);
			props = new Properties();
			try {
				props.load(fis);
				propsKeys = props.keySet();
			} catch (IOException ioe) {
				throw ioe;
			} finally {
				fis.close();
			}
			newMethods = new ArrayList<String>();
			for (String string : methods) {
				if(!propsKeys.contains(string)) newMethods.add(string);
			}
		}
		else return methods;
		
		return newMethods;
	}
	
	public static Map<String, String> checkPropsFileForUnused(File propsFile,
			List<String> methods) throws FileNotFoundException, IOException {
		FileInputStream fis;
		Map<String, String> unusedMethods;
		Set<Object> propsKeys;
		Properties props;
		String key;
		
		unusedMethods = new HashMap<String, String>();
		if(propsFile.exists()) {
			fis = new FileInputStream(propsFile);
			props = new Properties();
			try {
				props.load(fis);
				propsKeys = props.keySet();
			} catch (IOException ioe) {
				throw ioe;
			} finally {
				fis.close();
			}
			for (Object object : propsKeys) {
				key = (String) object;
				if(!methods.contains(object)) unusedMethods.put(key, props.getProperty(key));
			}
		}
		
		return unusedMethods;
	}
	
	/**
	 * This method updates the properties file with the given entries.
	 * 
	 * @param propsFile the properties file
	 * @param cmMap a map containing a class path - method path key-value pair
	 * @throws IOException thrown by <code>File</code>, <code>FileWriter</code>
	 * and <code>BufferedWriter</code> methods
	 */
	public static void updatePropsFile(File propsFile, Map<String, List<String>> cmMap) throws IOException {
		BufferedWriter bw;
		FileWriter fw;
		
		if(!propsFile.exists()) {
			propsFile.createNewFile();
		}
		fw = new FileWriter(propsFile, true);
		bw = new BufferedWriter(fw);
		
		try {
			bw.newLine();
			bw.newLine();
			bw.append(HASH);
			bw.append(SPACE);
			bw.append(AUTO_GENERATED_ENTRIES_MESSAGE);
			
			for (String key : cmMap.keySet()) {
				bw.newLine();
				bw.append(HASH);
				bw.append(SPACE);
				bw.append(key);
				for (String methodName : cmMap.get(key)) {
					bw.newLine();
					bw.append(methodName);
					bw.append(EQUALS);
					bw.append((methodName.equals(DEBUG_REGEX) ? FALSE_REGEX : DEFAULT_MESSAGE));
				}
			}
		} catch (IOException ioe) {
			throw ioe;
		} finally {
			bw.close();
		}
	}

	/**
	 * This method updates the properties file with the given entries.<br>
	 * A collection for the new entries and another for the unused entries
	 * will be received, so it can include the new ones and separate and
	 * comment the unused entries.
	 * 
	 * @param propsFile the properties file
	 * @param cmMap a map containing a class path - method path key-value pair
	 * @param unused a list containing the unused entries (full line, with
	 * everything that is on it)
	 * @throws IOException thrown by <code>File</code>, <code>FileReader</code>
	 * , <code>FileWriter</code>, <code>BufferedReader</code> and
	 * <code>BufferedWriter</code> methods
	 */
	public static void updatePropsFile(File propsFile,
			Map<String, List<String>> cmMap, List<String> unused)
			throws IOException {
		BufferedWriter bw, tempBw;
		BufferedReader br, tempBr;
		FileWriter fw, tempFw;
		FileReader fr, tempFr;
		File tempPropsFile;
		String currentLine, currentKey;
		List<String> current;
		int endIndex;
		
		current = new ArrayList<String>();
		tempPropsFile = File.createTempFile(TEMP_PROPS_FILE_PREFIX, TEMP_PROPS_FILE_SUFFIX);
		tempFw = new FileWriter(tempPropsFile);
		tempBw = new BufferedWriter(tempFw);
		if(!propsFile.exists()) propsFile.createNewFile();
		else {
			fr = new FileReader(propsFile);
			br = new BufferedReader(fr);
			try {
				while((currentLine = br.readLine()) != null) {
					if(currentLine.contains(EQUALS)) {
						endIndex = currentLine.indexOf(EQUALS);
						currentKey = currentLine.substring(0, endIndex);
						if(!unused.contains(currentKey)) {
							tempBw.append(currentLine);
							tempBw.newLine();
						}
						else current.add(currentLine);
					}
					else {
						tempBw.append(currentLine);
						tempBw.newLine();
					}
				}
			} catch (IOException ioe) {
				tempBw.close();
				throw ioe;
			} finally {
				br.close();
			}
		}
		
		try {
			tempBw.newLine();
			tempBw.newLine();
			tempBw.append(HASH);
			tempBw.append(SPACE);
			tempBw.append(AUTO_GENERATED_ENTRIES_MESSAGE);
			
			tempBw.newLine();
			tempBw.append(HASH);
			tempBw.append(SPACE);
			tempBw.append(AUTO_GENERATED_NEW_ENTRIES_MESSAGE);
			for (String key : cmMap.keySet()) {
				tempBw.newLine();
				tempBw.append(HASH);
				tempBw.append(SPACE);
				tempBw.append(key);
				for (String methodName : cmMap.get(key)) {
					tempBw.newLine();
					tempBw.append(methodName);
					tempBw.append(EQUALS);
					tempBw.append((methodName.equals(DEBUG_REGEX) ? FALSE_REGEX : DEFAULT_MESSAGE));
				}
			}
			
			tempBw.newLine();
			tempBw.append(HASH);
			tempBw.append(SPACE);
			tempBw.append(AUTO_GENERATED_UNUSED_ENTRIES_MESSAGE);
			for (String line : current) {
				tempBw.newLine();
				tempBw.append(HASH);
				tempBw.append(line);
			}
			tempBw.newLine();
		} catch (IOException ioe) {
			throw ioe;
		} finally {
			tempBw.close();
		}
		if(!tempPropsFile.renameTo(propsFile)) {
			fw = new FileWriter(propsFile);
			bw = new BufferedWriter(fw);
			tempFr = new FileReader(tempPropsFile);
			tempBr = new BufferedReader(tempFr);
			try {
				while((currentLine = tempBr.readLine()) != null) {
					bw.append(currentLine);
					bw.newLine();
				}
				tempPropsFile.deleteOnExit();
			} catch (IOException ioe) {
				throw ioe;
			} finally {
				tempBr.close();
				bw.close();
			}
		}
	}
	
	/**
	 * This method transform the class file path into a class path.
	 * 
	 * @param file the file of the class
	 * @return the class path
	 */
	private static String extractClassName(File file) {
		String className, tempName;
		Pattern pat;
		Matcher mat;
		int start, end;
		
		pat = Pattern.compile(CLASS_FILE_PATH_REGEX);
		mat = pat.matcher(file.getPath());
		tempName = mat.replaceAll(DOT);
		start = tempName.indexOf(DOT) + 1;
		end = tempName.lastIndexOf(DOT);
		className = tempName.substring(start, end);
		
		return className;
	}
	
	/**
	 * This method retrieves all the methods from a given <code>Class</code>.
	 * 
	 * @param c the <code>Classe</code> of which the methods will be loaded from
	 * @return a list containing the name of each method of the given class
	 * @throws SecurityException thrown by <code>java.lang.Class.getDeclaredMethods()</code>
	 */
	private static List<String> getMethods(Class<?> c) throws SecurityException {
		List<String> methodsName;
		Method[] currentMethods;
		
		methodsName = new ArrayList<String>();
		currentMethods = c.getDeclaredMethods();
		for (Method method : currentMethods) {
			methodsName.add(c.getCanonicalName() + DOT + method.getName());
		}
		
		return methodsName;
	}
	
	/**
	 * This method dynamically loads a class from a given class name
	 * 
	 * @param className the name of the class that has to be loaded
	 * @return the <code>Class</code> of the given class name
	 * @throws ClassNotFoundException thrown by <code>java.lang.ClassLoader.loadClass(String name)</code> 
	 */
	private static Class<?> getClass(String className) throws ClassNotFoundException {
		ClassLoader cl = ClassLoader.getSystemClassLoader();
		
		return cl.loadClass(className);
	}
}